package fsmonitor

import (
	"os"
	"path/filepath"
)

type Observer struct {
	watchPath   string
	initialized bool
	listener    Listener
	infoMap     map[string]os.FileInfo
}

func (o *Observer) init() {
	o.initialized = true
	infoMap := make(map[string]os.FileInfo)
	info, err := os.Stat(o.watchPath)
	if err == nil {
		if info.IsDir() {
			filepath.Walk(o.watchPath, func(path string, info os.FileInfo, err error) error {
				if err != nil || !o.listener.Accept(path, info) {
					return nil
				}
				infoMap[path] = info
				return nil
			})
		} else {
			infoMap[o.watchPath] = info
		}
	}
	o.infoMap = infoMap
}

func (o *Observer) check() {
	newMap := make(map[string]os.FileInfo)
	info, err := os.Stat(o.watchPath)
	if err != nil {
		o.checkInfo(o.watchPath, nil)
	} else {
		if info.IsDir() {
			filepath.Walk(o.watchPath, func(path string, info os.FileInfo, err error) error {
				if err != nil || !o.listener.Accept(path, info) {
					return nil
				}
				o.checkInfo(path, info)
				newMap[path] = info
				return nil
			})
		} else {
			o.checkInfo(o.watchPath, info)
			newMap[o.watchPath] = info
		}
	}
	for path := range o.infoMap {
		_, ok := newMap[path]
		if !ok {
			o.checkInfo(path, nil)
		}
	}
	o.infoMap = newMap
}

func (o *Observer) checkInfo(fullPath string, currentInfo os.FileInfo) {
	oldInfo := o.infoMap[fullPath]
	// new file/dir is added
	if oldInfo == nil {
		if currentInfo == nil {
			return
		}

		if currentInfo.IsDir() {
			o.listener.DirCreated(fullPath, currentInfo)
		} else {
			o.listener.FileCreated(fullPath, currentInfo)
		}
		delete(o.infoMap, fullPath)
		return
	}

	// existing file/dir is deleted
	if currentInfo == nil {
		if oldInfo.IsDir() {
			o.listener.DirDeleted(fullPath, oldInfo)
		} else {
			o.listener.FileDeleted(fullPath, oldInfo)
		}
		return
	}

	// same path file removed as well as dir is created
	if oldInfo.IsDir() != currentInfo.IsDir() {
		if currentInfo.IsDir() {
			o.listener.FileDeleted(fullPath, oldInfo)
			o.listener.DirCreated(fullPath, currentInfo)
		} else {
			o.listener.DirDeleted(fullPath, oldInfo)
			o.listener.FileCreated(fullPath, currentInfo)
		}
		delete(o.infoMap, fullPath)
		return
	}

	// dir changed
	if currentInfo.IsDir() && !currentInfo.ModTime().Equal(oldInfo.ModTime()) {
		o.listener.DirChanged(fullPath, currentInfo)
	}

	// file changed
	if !currentInfo.IsDir() && !currentInfo.ModTime().Equal(oldInfo.ModTime()) {
		o.listener.FileChanged(fullPath, currentInfo)
	}
	delete(o.infoMap, fullPath)
}

func (o *Observer) run() {
	if o.listener == nil {
		return
	}
	if o.initialized {
		o.check()
	} else {
		o.init()
	}
}

func NewObserver(watchPath string, listener Listener) *Observer {
	return &Observer{
		watchPath: watchPath,
		listener:  listener,
	}
}
